﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Mime
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Text;

    using Mews.IO;

    /// <summary>Decodes encoded words in header fields.</summary>
    internal struct EncodedWordDecoder
    {
        private readonly List<byte> currentBytes;
        private readonly List<Token> tokens;
        private int totalLength;
        private TokenKind lastKind;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal EncodedWordDecoder(int tokenCapacity, int tokensCapacity)
        {
            this.currentBytes = new List<byte>(tokenCapacity);
            this.tokens = new List<Token>(tokensCapacity);
            this.totalLength = 0;
            this.lastKind = TokenKind.Normal;
        }

        internal void ProcessEvent(byte ev, bool isQuoted)
        {
            var currentKind = GetKind(ev, isQuoted);

            if (currentKind != this.lastKind)
            {
                this.AddTokenIfValid();
                this.lastKind = currentKind;
            }

            this.currentBytes.Add(ev);
            ++this.totalLength;
        }

        internal string GetResult()
        {
            this.AddTokenIfValid();

            if (this.tokens.Count == 0)
            {
                return string.Empty;
            }
            else if (this.tokens.Count == 1)
            {
                return DecodeLeadingOrTrailing(this.tokens[0]);
            }

            StringBuilder body = new StringBuilder(this.totalLength);
            body.Append(DecodeLeadingOrTrailing(this.tokens[0]));

            for (int index = 1; index < this.tokens.Count - 1; ++index)
            {
                // Whitespace between two encoded words must be ignored
                if ((this.tokens[index].Kind != TokenKind.Whitespace) ||
                    (this.tokens[index - 1].Kind != TokenKind.Encoded) ||
                    (this.tokens[index + 1].Kind != TokenKind.Encoded))
                {
                    body.Append(this.tokens[index].Word);
                }
            }

            body.Append(DecodeLeadingOrTrailing(this.tokens[this.tokens.Count - 1]));
            return body.ToString();
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private void AddTokenIfValid()
        {
            if (this.currentBytes.Count > 0)
            {
                this.tokens.Add(new Token(this.lastKind, this.currentBytes));
                this.currentBytes.Clear();
            }
        }

        private static TokenKind GetKind(byte ev, bool isQuoted)
        {
            if (isQuoted)
            {
                return TokenKind.Quoted;
            }
            else if ((ev == (byte)' ') || (ev == (byte)'\t'))
            {
                return TokenKind.Whitespace;
            }
            else
            {
                return TokenKind.Normal;
            }
        }

        private static string DecodeLeadingOrTrailing(Token token)
        {
            return token.Kind == TokenKind.Whitespace ? string.Empty : token.Word;
        }

        private enum TokenKind
        {
            Quoted,
            Whitespace,
            Normal,
            Encoded
        }

        /// <summary>Represents a token in a header field body.</summary>
        private struct Token
        {
            private readonly TokenKind kind;
            private readonly string word;

            ////////////////////////////////////////////////////////////////////////////////////////////////////////////

            internal Token(TokenKind kind, List<byte> bytes)
            {
                if (kind == TokenKind.Normal)
                {
                    this.kind = TryDecodeEncodedWord(bytes, out this.word) ? TokenKind.Encoded : TokenKind.Normal;
                }
                else
                {
                    this.kind = kind;
                    this.word = FastAsciiDecode(bytes);
                }
            }

            internal TokenKind Kind
            {
                get { return this.kind; }
            }

            internal string Word
            {
                get { return this.word; }
            }

            ////////////////////////////////////////////////////////////////////////////////////////////////////////////

            private static bool TryDecodeEncodedWord(List<byte> bytes, out string word)
            {
                const int MinEncodedWordLength = 8; // Charset with one character and encoded text with the length of 0.
                int count = bytes.Count;

                // We're deliberately not checking for maximum word length, as mandated in RFC 2047 2., because some
                // clients have been observed to produce longer words. Also, limiting the length has no advantages with
                // our approach.
                if ((count >= MinEncodedWordLength) && (bytes[0] == (byte)'=') && (bytes[1] == (byte)'?') &&
                    (bytes[count - 2] == (byte)'?') && (bytes[count - 1] == (byte)'='))
                {
                    int firstSeparatorIndex;

                    // We know that there is a ? at index length - 2
                    for (firstSeparatorIndex = 3; bytes[firstSeparatorIndex] != (byte)'?'; ++firstSeparatorIndex)
                    {
                    }

                    if ((firstSeparatorIndex < count - 4) && bytes[firstSeparatorIndex + 2] == (byte)'?')
                    {
                        Encoding encoding = EncodingHelper.GetEncoding(FastAsciiDecode(bytes, 2, firstSeparatorIndex - 2));

                        if (encoding != null)
                        {
                            byte[] decodedToken;
                            int decodedLength = TryDecodeToken(bytes, firstSeparatorIndex + 1, out decodedToken);

                            if (decodedLength >= 0)
                            {
                                word = encoding.GetString(decodedToken, 0, decodedLength);
                                return true;
                            }
                        }
                    }
                }

                word = FastAsciiDecode(bytes, 0, count);
                return false;
            }

            private static int TryDecodeToken(List<byte> bytes, int encodingCharacterIndex, out byte[] decodedToken)
            {
                var encodedLength = bytes.Count - encodingCharacterIndex - 4;
                decodedToken = new byte[encodedLength];

                if (encodedLength == 0)
                {
                    return 0;
                }

                var stream = new MemoryStream(bytes.ToArray(), encodingCharacterIndex + 2, encodedLength);

                switch (Char.ToUpper((char)bytes[encodingCharacterIndex], CultureInfo.InvariantCulture))
                {
                    case 'B':
                        return Decode(new Base64Stream(stream, encodedLength), decodedToken);
                    case 'Q':
                        return Decode(new QuotedPrintableStream(stream, encodedLength, true), decodedToken);
                    default:
                        return -1;
                }
            }

            private static int Decode(Stream stream, byte[] buffer)
            {
                using (stream)
                {
                    int length = 0;
                    int read;

                    while ((read = stream.Read(buffer, length, buffer.Length - length)) > 0)
                    {
                        length += read;
                    }

                    return length;
                }
            }

            private static string FastAsciiDecode(List<byte> bytes)
            {
                return FastAsciiDecode(bytes, 0, bytes.Count);
            }

            private static string FastAsciiDecode(List<byte> bytes, int offset, int count)
            {
                var characters = new char[count];
                int pastEnd = offset + count;
                int charIndex = 0;

                for (int index = offset; index < pastEnd; ++index)
                {
                    characters[charIndex++] = (char)bytes[index];
                }

                return new string(characters);
            }
        }
    }
}
